<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>私有攻略库问答（Vue + Axios）</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif; background: #f6f7fb; color: #222; }
    .container { max-width: 920px; margin: 24px auto; padding: 0 16px; }
    .card { background: #fff; border-radius: 12px; box-shadow: 0 6px 16px rgba(0,0,0,.06); padding: 20px; margin-bottom: 16px; }
    .title { font-size: 20px; font-weight: 700; margin: 0 0 12px; }
    label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
    textarea, input[type="number"], input[type="text"] { width: 100%; padding: 12px 12px; border: 1px solid #e6e6e6; border-radius: 10px; outline: none; font-size: 14px; background: #fcfcfd; }
    textarea:focus, input:focus { border-color: #6a7cff; box-shadow: 0 0 0 3px rgba(106,124,255,.12); }
    .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
    .row .col { flex: 1; min-width: 180px; }
    .btn { appearance: none; border: none; cursor: pointer; border-radius: 10px; padding: 10px 16px; font-size: 14px; font-weight: 600; color: #fff; background: linear-gradient(135deg, #6a7cff, #4c63f7); box-shadow: 0 6px 14px rgba(76,99,247,.3); }
    .btn:disabled { opacity: .6; cursor: not-allowed; }
    .answer { white-space: pre-wrap; line-height: 1.7; border-radius: 10px; background: #fafafe; border: 1px solid #eee; padding: 16px; }
    .muted { color: #888; font-size: 13px; }
    .error { color: #d23c3c; background: #fff3f3; border: 1px solid #ffd8d8; padding: 10px 12px; border-radius: 8px; }
    .note { border: 1px solid #eee; border-radius: 10px; padding: 12px; background: #fff; }
    .note-id { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: #666; font-size: 12px; }
    .note-title { font-weight: 700; margin: 4px 0 6px; }
    .footer-tip { text-align: center; color: #999; font-size: 12px; margin-top: 24px; }
    .loading { display: inline-flex; align-items: center; gap: 8px; color: #4c63f7; }
    .dot { width: 8px; height: 8px; background: #4c63f7; border-radius: 50%; animation: b .9s infinite ease-in-out; }
    .dot:nth-child(2) { animation-delay: .15s; }
    .dot:nth-child(3) { animation-delay: .3s; }
    @keyframes b { 0%,80%,100%{ transform: scale(0); } 40%{ transform: scale(1); } }
    .kbd { display: inline-block; border: 1px solid #ddd; border-bottom-width: 2px; padding: 0 6px; border-radius: 6px; font-size: 12px; background: #f8f8fa; }
    .link { color: #4c63f7; text-decoration: none; }
  </style>
</head>
<body>
  <div id="app" class="container">
    <div class="card">
      <h2 class="title">私有攻略库问答</h2>
      <div class="row" style="margin-bottom: 10px;">
        <div class="col">
          <label>问题（按 <span class="kbd">Ctrl</span> + <span class="kbd">Enter</span> 提交）</label>
          <textarea v-model.trim="question" rows="5" placeholder="请输入你的问题，例如：如何在项目中集成大模型？"></textarea>
        </div>
      </div>

      <div class="row" style="margin-bottom: 14px;">
        <div class="col" style="max-width: 160px;">
          <label>topK（检索条数 1-10）</label>
          <input type="number" v-model.number="topK" min="1" max="10" />
        </div>
        <div class="col" style="max-width: 360px;">
          <label>接口地址（为空时自动推断）</label>
          <input type="text" v-model.trim="apiBase" placeholder="例如：http://localhost:8080" />
        </div>
        <div class="col" style="flex: 0; display: flex; gap: 8px;">
          <div>
            <label>&nbsp;</label>
            <button class="btn" :disabled="disabledAsk" @click="ask">
              {{ loading ? '请求中...' : '发送提问' }}
            </button>
          </div>
          <div>
            <label>&nbsp;</label>
            <button class="btn" style="background: #29a36a;" :disabled="loading" @click="ping">
              测试连接
            </button>
          </div>
        </div>
      </div>

      <div class="muted">提示：默认请求 <code>POST /chat</code>，body 为 <code>{ question, topK }</code>。若页面通过 file:// 打开，会自动使用 <code>http://localhost:8080</code>。</div>
    </div>

    <div class="card">
      <h3 class="title" style="margin-bottom: 10px;">回答</h3>

      <div v-if="error" class="error" style="margin-bottom: 12px;">{{ error }}</div>

      <div v-if="loading" class="loading" style="margin-bottom: 12px;">
        <span class="dot"></span><span class="dot"></span><span class="dot"></span> 正在生成回答...
      </div>

      <div v-if="!loading && answer" class="answer">{{ answer }}</div>
      <div v-else-if="!loading" class="muted">暂无回答</div>
    </div>

    <div class="card">
      <h3 class="title" style="margin-bottom: 10px;">引用的私有攻略</h3>
              <div v-if="strategies && strategies.length">
          <div v-for="n in strategies" :key="n.id" class="note" style="margin-bottom: 10px;">
          <div class="note-id">ID: {{ n.id }}</div>
          <div class="note-title">{{ n.title }}</div>
          <div v-if="n.subTitle" class="muted" style="font-style: italic; color: #666;">{{ n.subTitle }}</div>
          <div class="muted">{{ n.summary }}</div>
        </div>
      </div>
      <div v-else class="muted">本次回答未引用私有攻略或结果为空</div>
    </div>

    <div class="footer-tip">
      如需录入数据，请调用 <code>POST /strategies</code> 接口。按 <span class="kbd">Ctrl</span> + <span class="kbd">Enter</span> 快速提交。
    </div>
  </div>

  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.global.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.5.0/axios.min.js"></script>
  <script>
    // 捕获未处理的 Promise 错误，避免浏览器扩展引起的噪音报错
    window.addEventListener('unhandledrejection', function (event) {
      if (event && event.reason && /Receiving end does not exist/i.test(String(event.reason))) {
        event.preventDefault();
      }
    });

    const app = Vue.createApp({
      data() {
        return {
          question: '',
          topK: 3,
          apiBase: '',
          answer: '',
          strategies: [],
          loading: false,
          error: ''
        }
      },
      computed: {
        disabledAsk() {
          return this.loading || !this.question || this.topK < 1 || this.topK > 10;
        },
        baseURL() {
          if (this.apiBase && this.apiBase.trim()) return this.apiBase.trim().replace(/\/$/, '');
          if (location.protocol === 'file:') return 'http://localhost:8080';
          return window.location.origin;
        }
      },
      methods: {
        async ask() {
          if (this.disabledAsk) return;
          this.loading = true;
          this.error = '';
          this.answer = '';
          this.strategies = [];
          try {
            const resp = await axios.post(this.baseURL + '/chat', { question: this.question, topK: this.topK }, {
              headers: { 'Content-Type': 'application/json' },
              timeout: 60000,
              withCredentials: false
            });
            this.answer = resp?.data?.answer || '(无回答)';
            this.strategies = Array.isArray(resp?.data?.strategies) ? resp.data.strategies : [];
          } catch (e) {
            this.error = this.formatError(e);
          } finally {
            this.loading = false;
          }
        },
        async ping() {
          this.error = '';
          try {
            await axios.get(this.baseURL + '/__ping__', { timeout: 5000 }).catch(() => {});
            await axios({ method: 'OPTIONS', url: this.baseURL + '/strategies', timeout: 5000 }).catch(() => {});
            alert('与后端通信正常（或已通过 CORS 预检）。');
          } catch (e) {
            alert('连接失败：' + this.formatError(e));
          }
        },
        formatError(e) {
          const status = e?.response?.status;
          if (status === 0) return '网络不可达，请检查接口地址或代理设置。';
          const msg = e?.response?.data?.message || e?.message || '请求失败';
          if (status) return `HTTP ${status}: ${msg}`;
          return msg;
        }
      },
      mounted() {
        window.addEventListener('keydown', (ev) => {
          if (ev.ctrlKey && ev.key === 'Enter') this.ask();
        });
      }
    });
    app.mount('#app');
  </script>
</body>
</html>



